<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\Network;

use Socket;
use Srv\Libs\Common\CommString;
use Srv\Libs\Common\CommValid;

class NetDns
{
    private $socketUdp;                                     // socket的udp协议
    private int $fpCurr                 = 0;                // 指针偏移量
    private array $answersArray         = ['C0' => 0];      // 偏移量对应

    /**
     * __construct
     */
    public function __construct()
    {
        !defined('AF_INET')     && define('AF_INET', 2);
        !defined('SOCK_DGRAM')  && define('SOCK_DGRAM', 2);
    }

    /**
     * __destruct
     */
    public function __destruct()
    {
        if(is_resource($this->socketUdp) || $this->socketUdp instanceof Socket){
            @socket_close($this->socketUdp);
            $this->socketUdp    = null;
        }
    }

    /**
     * @param string $domain
     * @param int $queryType
     * @param string $server
     * @param int $port
     * @param int $queryClass
     * @return array
     * get
     */
    public function get(string $domain, int $queryType = self::QUERY_TYPE_A, string $server = '1.2.4.8', int $port = 53, int $queryClass = self::QUERY_CLASS_IN):array
    {
        $result     =[
            'result'            => 0,   // 结果[0-错误,1-成功,2-无结果,3-失败,4-DNS错误,5-超时]
            'serverName'        => '',
            'serverIp'          => '',
            'serverPort'        => 0,
            'domain'            => '',
            'queryType'         => '',
            'queryClass'        => '',
            'questionsNum'      => 0,
            'answersNum'        => 0,
            'authorityNum'      => 0,
            'additionalNum'     => 0,
            'questionList'      => [],
            'answerList'        => [],
            'authorityList'     => [],
            'additionalList'    => [],
        ];
        if(!(is_resource($this->socketUdp) || $this->socketUdp instanceof Socket)) $this->socketUdp = @socket_create(AF_INET, SOCK_DGRAM, 17);  // 17-UDP
        if(!(is_resource($this->socketUdp) || $this->socketUdp instanceof Socket)) return $result;
        if($queryType === self::QUERY_TYPE_PTR && CommValid::isIp($domain)){    // IPv4反解析
            $domain             = implode('.', array_reverse(explode('.', $domain))).'.in-addr.arpa';
        }
        $serverIp               = @gethostbyname($server);
        $result['serverName']   = $server;
        $result['serverIp']     = $serverIp;
        $result['serverPort']   = $port;
        $result['domain']       = $domain;
        $result['queryType']    = $this->getQueryTypeNameByNum($queryType);
        $result['queryClass']   = $this->getQueryClassNameByNum($queryClass);
        $pack                   = $this->buildUdpPack($domain, $queryType, $queryClass);
        if(strlen($pack) < 1) return $result;
        $socketSendto           = @socket_sendto($this->socketUdp, $pack, strlen($pack), 0, $serverIp, $port);
        if(!$socketSendto){
            $result['result']           = 4;      // DNS错误
            return $result;
        }
        $read           = [$this->socketUdp];
        $write          = $except           = [];
        $reply          = $recvAddr         = '';
        $recvPort       = -1;
        $timeOut        = 5;
        @socket_select($read, $write, $except, $timeOut);
        if(count($read) > 0){
            @socket_recvfrom($this->socketUdp, $reply, 512, 0, $recvAddr, $recvPort);
            unset($recvPort);
            $replyLength                = strlen($reply);
            if($replyLength < 1){
                $result['result']       = 3;        // 查询失败
                return $result;
            }
            $fpCurr                     = 4;        // 忽略标识符、代码和表示
            $result['questionsNum']     = intval(bindec(CommString::strToBin(substr($reply, $fpCurr, 2))));         // 问题数
            $result['answersNum']       = intval(bindec(CommString::strToBin(substr($reply, ($fpCurr += 2), 2))));  // 答案数
            $result['authorityNum']     = intval(bindec(CommString::strToBin(substr($reply, ($fpCurr += 2), 2))));  // 权威答案数
            $result['additionalNum']    = intval(bindec(CommString::strToBin(substr($reply, ($fpCurr += 2), 2))));  // 附加答案数
            $this->fpCurr               = $fpCurr + 2;
            if($result['answersNum'] < 1 && $result['authorityNum'] < 1 && $result['additionalNum'] < 1){
                $result['result']       = 2;        // 查询无结果
                return $result;
            }else{
                $result['result']       = 1;
            }
            $result['questionList']     = $this->getQuestions($reply);
            for($i = 0; $i < $result['answersNum']; $i++) $result['answerList'][] = $this->getAnswers($reply);
            for($i = 0; $i < $result['authorityNum']; $i++) $result['authorityList'][]  = $this->getAnswers($reply);
            for($i = 0; $i < $result['additionalNum']; $i++) $result['additionalList'][] = $this->getAnswers($reply);
        }else{
            $result['result']           = 5;        // 超时
            return $result;
        }
        return $result;
    }

    /**
     * @param string $pack
     * @return array
     * getQuestions
     */
    private function getQuestions(string $pack):array
    {
        $result['domain']   = $this->getDomain($pack);
        $fpCurr             = $this->fpCurr;
        $result['type']     = $this->getQueryTypeNameByNum(intval(bindec(CommString::strToBin(substr($pack, $fpCurr, 2)))));
        $result['class']    = $this->getQueryClassNameByNum(intval(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))));
        $this->fpCurr       = $fpCurr + 2;
        return $result;
    }

    /**
     * @param string $pack
     * @return array
     * getAnswers
     */
    private function getAnswers(string $pack):array
    {
        $fpCurr         = $this->fpCurr;
        $answersDec     = bindec(CommString::strToBin(substr($pack, $fpCurr, 1)));
        $answersCurr    = strtoupper(dechex($answersDec));
        if($answersDec > 191 && !isset($this->answersArray[$answersCurr])){
            $this->answersArray[$answersCurr] = $fpCurr;
        }
        $this->fpCurr = $fpCurr += 2;
        $result             = [];
        $result['type']     = $this->getQueryTypeNameByNum(intval(bindec(CommString::strToBin(substr($pack, $fpCurr, 2)))));
        $result['class']    = $this->getQueryClassNameByNum(intval(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))));
        $result['ttl']      = bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 4)));
        $length             = bindec(CommString::strToBin(substr($pack, ($fpCurr += 4), 2)));
        $fpCurr             += 2;
        $this->fpCurr     = $fpCurr;
        switch($result['type']){
            case 'A' : {
                $result['ip'] = bindec(CommString::strToBin(substr($pack, $fpCurr++, 1))).'.';
                $result['ip'] .= bindec(CommString::strToBin(substr($pack, $fpCurr++, 1))).'.';
                $result['ip'] .= bindec(CommString::strToBin(substr($pack, $fpCurr++, 1))).'.';
                $result['ip'] .= bindec(CommString::strToBin(substr($pack, $fpCurr++, 1)));
                $this->fpCurr = $fpCurr;
                break;
            }
            case 'MX' : {
                $result['preference'] = bindec(CommString::strToBin(substr($pack, $fpCurr, 2)));
                $this->fpCurr += 2;
                $result['domain'] = $this->getDomain($pack);
                break;
            }
            case 'PTR':
            case 'NS':
            case 'CNAME' : {
                $result['domain'] = $this->getDomain($pack);
                break;
            }
            case 'SOA' : {
                $result['mname']    = $this->getDomain($pack);
                $result['rname']    = $this->getDomain($pack);
                $fpCurr             = $this->fpCurr;
                $result['serial']   = bindec(CommString::strToBin(substr($pack, $fpCurr, 4)));
                $result['refresh']  = bindec(CommString::strToBin(substr($pack, ($fpCurr += 4), 4)));
                $result['retry']    = bindec(CommString::strToBin(substr($pack, ($fpCurr += 4), 4)));
                $result['expire']   = bindec(CommString::strToBin(substr($pack, ($fpCurr += 4), 4)));
                $result['mininmum'] = bindec(CommString::strToBin(substr($pack, ($fpCurr += 4), 4)));
                $this->fpCurr     = $fpCurr + 4;
                break;
            }
            case 'SRV' : {
                $result['priority'] = bindec(CommString::strToBin(substr($pack, $fpCurr, 2)));
                $result['weight']   = bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)));
                $result['port']     = bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)));
                $this->fpCurr     = $fpCurr + 2;
                $result['hostname'] = $this->getDomain($pack);
                break;
            }
            case 'AAAA' : {
                $ipv6               = [];
                $ipv6[0] = str_pad(dechex(bindec(CommString::strToBin(substr($pack, $fpCurr, 2)))), 4, '0', STR_PAD_LEFT);
                $ipv6[1] = str_pad(dechex(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))), 4, '0', STR_PAD_LEFT);
                $ipv6[2] = str_pad(dechex(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))), 4, '0', STR_PAD_LEFT);
                $ipv6[3] = str_pad(dechex(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))), 4, '0', STR_PAD_LEFT);
                $ipv6[4] = str_pad(dechex(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))), 4, '0', STR_PAD_LEFT);
                $ipv6[5] = str_pad(dechex(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))), 4, '0', STR_PAD_LEFT);
                $ipv6[6] = str_pad(dechex(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))), 4, '0', STR_PAD_LEFT);
                $ipv6[7] = str_pad(dechex(bindec(CommString::strToBin(substr($pack, ($fpCurr += 2), 2)))), 4, '0', STR_PAD_LEFT);
                $this->fpCurr       = $fpCurr + 2;
                $ipv6a              = implode(':', $ipv6);
                $result['ipv6all']  = $ipv6a;
                $ipv6b              = $ipv6c = '';
                foreach($ipv6 as $key => $val){
                    if($key < 7){
                        $ipv6b .= ($val == '0000' ? (substr($ipv6b, -2) == '::' ? '' : '::') : (($ipv6b != '' && substr($ipv6b, -1) != ':') ? ':' : '').$val);
                        $ipv6c .= ($val == '0000' ? (substr($ipv6c, -2) == '::' ? '' : '::') : (($ipv6c != '' && substr($ipv6c, -1) != ':') ? ':' : '').ltrim($val, '0'));
                    }else{
                        $ipv6b .= (($ipv6b != '' && substr($ipv6b, -1) != ':') ? ':' : '').$val;
                        $ipv6c .= (($ipv6b != '' && substr($ipv6b, -1) != ':') ? ':' : '').($val == '0000' ? '0' : ltrim($val, '0'));
                    }
                }
                $result['ipv6min']  = $ipv6b;
                $result['ipv6']     = $ipv6c;
                break;
            }
            case 'TXT' : {
                $result['spf']  = substr($pack, $fpCurr, $length);
                $this->fpCurr += $length;
                break;
            }
            default : {
                $result['data'] = substr($pack, $fpCurr, $length);
                $this->fpCurr += $length;
            }
        }
        return $result;
    }

    /**
     * @param string $pack
     * @return string
     * getDomain
     */
    private function getDomain(string $pack):string
    {
        $fpCurr         = $this->fpCurr;
        $fpStart        = $fpCurr;
        $domain         = '';
        $lengthToo      = -1;
        $fpStartLast    = 0;
        while(true){
            if(strlen($pack) < $fpStart) break;
            $length = bindec(CommString::strToBin(substr($pack, $fpStart, 1)));
            if($length > 0){
                if($length == 192){    // \xC0
                    if($lengthToo == -1) $fpCurr += 2;
                    $fpStart = $lengthToo = bindec(CommString::strToBin(substr($pack, $fpStart+1, 1)));         // 偏移量[(cn-c0)*256+0c]
                    if($fpStart < 1) break;
                }else if($length == 193){    // \xC1
                    if($lengthToo == -1) $fpCurr += 2;
                    $fpStart = $lengthToo = 256+bindec(CommString::strToBin(substr($pack, $fpStart+1, 1)));     // 偏移量
                    if($fpStart < 1) break;
                    if($lengthToo == -1) $fpCurr += 2;
                    $fpStartLast = $fpStart + 2;
                    $fpStart = bindec(CommString::strToBin(substr($pack, $fpStart+1, 1)));                      // 偏移量
                    if($fpStart < 1) break;
                    if(isset($this->answersArray['C1'])) $fpStart = $this->answersArray['C1'] - $fpStart;
                }else{
                    $domain .= substr($pack, ($fpStart += 1), $length).'.';
                    if($fpStartLast > 0){
                        break;
                    }else{
                        $fpStart += $length;
                    }
                    if($lengthToo == -1) $fpCurr += $length + 1;
                }
            }else{    // \x00结束
                if($lengthToo == -1) $fpCurr += 1;
                break;
            }
        }
        $this->fpCurr = $fpCurr;
        return $domain;
    }

    /**
     * @param string $domain
     * @param int $queryType
     * @param int $queryClass
     * @return string
     * buildUdpPack
     */
    private function buildUdpPack(string $domain, int $queryType, int $queryClass):string
    {
        $id                 = "\x00\x02";                                   // ID:占16位,2个字节此报文的编号,由客户端指定DNS回复时带上此标识,以指示处理的对应请应请求
        $qrBit              = "0";                                          // QR:占1位,1/8字节0代表查询,1代表DNS回复
        $opcodeBit          = "0000";                                       // Opcode:占4位,1/2字节指示查询种类：0:标准查询；1:反向查询；2:服务器状态查询；3-15:未使用
        $aaBit              = "0";                                          // AA:占1位,1/8字节是否权威回复
        $tcBit              = "0";                                          // TC:占1位,1/8字节因为一个UDP报文为512字节,所以该位指示是否截掉超过的部分
        $rdBit              = "1";                                          // RD:占1位,1/8字节此位在查询中指定,回复时相同设置为1指示服务器进行递归查询
        $raBit              = "0";                                          // RA:占1位,1/8字节由DNS回复返回指定,说明DNS服务器是否支持递归查询
        $zBit               = "000";                                        // Z:占3位,3/8字节保留字段,必须设置为0
        $rCodeBit           = "0000";                                       // RCODE:占4位,1/2字节由回复时指定的返回码：0:无差错；1:格式错；2:DNS出错；3:域名不存在；4:DNS不支持这类查询；5:DNS拒绝查询；6-15:保留字段
        $qdCount            = "\x00\x01";                                   // QDCOUNT:占16位,2字节一个无符号数指示查询记录的个数
        $anCount            = "\x00\x00";                                   // ANCOUNT:占16位,2字节一个无符号数指明回复记录的个数
        $nsCount            = "\x00\x00";                                   // NSCOUNT:占16位,2字节一个无符号数指明权威记录的个数
        $arCount            = "\x00\x00";                                   // ARCOUNT:占16位,2字节一个无符号数指明格外记录的个数
        $qName              = $this->buildDomain($domain);                  // QNAME:不定长,表示要查询的域名（两边的方框用 / 来表示不定长）
        if(strlen($qName) < 1) return '';
        $queryTypeValStr    = $this->getQueryTypeValStrByNum($queryType);   // QTYPE:2字节,根据RFC1035及nslookup的帮助文档,我定义以下枚举类型：
        if(strlen($queryTypeValStr) < 1) $queryTypeValStr = $this->getQueryTypeValStrByNum(self::QUERY_TYPE_A);
        $classTypeVal       = $this->getQueryClassValStrByNum($queryClass);
        if(strlen($classTypeVal) < 1) $classTypeVal = $this->getQueryClassValStrByNum(self::QUERY_CLASS_IN);
        return $id.chr(bindec($qrBit.$opcodeBit.$aaBit.$tcBit.$rdBit)).chr(bindec($raBit.$zBit.$rCodeBit)).$qdCount.$anCount.$nsCount.$arCount.$qName.$queryTypeValStr.$classTypeVal;
    }

    /**
     * @param string $domain
     * @return string
     * buildDomain
     */
    private function buildDomain(string $domain):string
    {
        $domainValStr       = '';
        foreach(explode('.', $domain) as $val){
            $len            = strlen(trim($val));
            if($len > 255 || $len < 1) return '';
            $domainValStr   .= chr($len).$val;
        }
        $domainValStr       .= "\x00";
        return $domainValStr;
    }

    /**
     * @param int $num
     * @return string
     * getQueryClassValStrByNum
     */
    private function getQueryClassValStrByNum(int $num):string
    {
        return $this->queryClassList[$num]['valStr']??'';
    }

    /**
     * @param int $num
     * @return string
     * getQueryClassNameByNum
     */
    private function getQueryClassNameByNum(int $num):string
    {
        return $this->queryClassList[$num]['name']??'';
    }

    /**
     * @param int $num
     * @return string
     * getQueryTypeValStrByNum
     */
    private function getQueryTypeValStrByNum(int $num):string
    {
        return $this->queryTypeList[$num]['valStr']??'';
    }

    /**
     * @param int $num
     * @return string
     * getQueryTypeNameByNum
     */
    private function getQueryTypeNameByNum(int $num):string
    {
        return $this->queryTypeList[$num]['name']??'';
    }

    public const QUERY_CLASS_IN         = 1;        // Internet
    public const QUERY_CLASS_CSNET      = 2;        // CSNET
    public const QUERY_CLASS_CHAOS      = 3;        // Chaos
    public const QUERY_CLASS_HESIOD     = 4;        // MIT Athena Hesiod
    public const QUERY_CLASS_ANY        = 255;      // Any
    private array $queryClassList   = [
        self::QUERY_CLASS_IN            => ['name' => 'IN',     'valStr' => "\x00\x01"],
        self::QUERY_CLASS_CSNET         => ['name' => 'CSNET',  'valStr' => "\x00\x02"],
        self::QUERY_CLASS_CHAOS         => ['name' => 'CHAOS',  'valStr' => "\x00\x03"],
        self::QUERY_CLASS_HESIOD        => ['name' => 'HESIOD', 'valStr' => "\x00\x04"],
        self::QUERY_CLASS_ANY           => ['name' => 'ANY',    'valStr' => "\x00\xFF"],
    ];
    public const QUERY_TYPE_A           = 1;        // IPv4
    public const QUERY_TYPE_NS          = 2;        // DNS服务
    public const QUERY_TYPE_MD          = 3;        // 邮件接收站
    public const QUERY_TYPE_MF          = 4;        // 邮件中转站
    public const QUERY_TYPE_CNAME       = 5;        // 别名
    public const QUERY_TYPE_SOA         = 6;        // DNS授权
    public const QUERY_TYPE_MB          = 7;        // 邮箱域名
    public const QUERY_TYPE_MG          = 8;        // 邮件组成员
    public const QUERY_TYPE_MR          = 9;        // 邮件重命名域名
    public const QUERY_TYPE_NULL        = 10;       // 空资源
    public const QUERY_TYPE_WKS         = 11;       // 已知服务
    public const QUERY_TYPE_PTR         = 12;       // IP反解析
    public const QUERY_TYPE_HINFO       = 13;       // 计算机CPU以及操作系统类型
    public const QUERY_TYPE_MINFO       = 14;       // 邮箱或邮件列表
    public const QUERY_TYPE_MX          = 15;       // 邮件
    public const QUERY_TYPE_TXT         = 16;       // 文本
    public const QUERY_TYPE_AAAA        = 28;       // IPv6
    public const QUERY_TYPE_SRV         = 33;       // 资源记录
    public const QUERY_TYPE_UINFO       = 100;      // 用户信息
    public const QUERY_TYPE_UID         = 101;      // 用户标识符
    public const QUERY_TYPE_GID         = 102;      // 组名的组标识符
    public const QUERY_TYPE_ANY         = 255;      // Any
    private array $queryTypeList    = [
        self::QUERY_TYPE_A              => ['name' => 'A',      'valStr' => "\x00\x01"],
        self::QUERY_TYPE_NS             => ['name' => 'NS',     'valStr' => "\x00\x02"],
        self::QUERY_TYPE_MD             => ['name' => 'MD',     'valStr' => "\x00\x03"],
        self::QUERY_TYPE_MF             => ['name' => 'MF',     'valStr' => "\x00\x04"],
        self::QUERY_TYPE_CNAME          => ['name' => 'CNAME',  'valStr' => "\x00\x05"],
        self::QUERY_TYPE_SOA            => ['name' => 'SOA',    'valStr' => "\x00\x06"],
        self::QUERY_TYPE_MB             => ['name' => 'MB',     'valStr' => "\x00\x07"],
        self::QUERY_TYPE_MG             => ['name' => 'MG',     'valStr' => "\x00\x08"],
        self::QUERY_TYPE_MR             => ['name' => 'MR',     'valStr' => "\x00\x09"],
        self::QUERY_TYPE_NULL           => ['name' => 'NULL',   'valStr' => "\x00\x0A"],
        self::QUERY_TYPE_WKS            => ['name' => 'WKS',    'valStr' => "\x00\x0B"],
        self::QUERY_TYPE_PTR            => ['name' => 'PTR',    'valStr' => "\x00\x0C"],
        self::QUERY_TYPE_HINFO          => ['name' => 'HINFO',  'valStr' => "\x00\x0D"],
        self::QUERY_TYPE_MINFO          => ['name' => 'MINFO',  'valStr' => "\x00\x0E"],
        self::QUERY_TYPE_MX             => ['name' => 'MX',     'valStr' => "\x00\x0F"],
        self::QUERY_TYPE_TXT            => ['name' => 'TXT',    'valStr' => "\x00\x10"],
        self::QUERY_TYPE_AAAA           => ['name' => 'AAAA',   'valStr' => "\x00\x1C"],
        self::QUERY_TYPE_SRV            => ['name' => 'SRV',    'valStr' => "\x00\x21"],
        self::QUERY_TYPE_UINFO          => ['name' => 'UINFO',  'valStr' => "\x00\x64"],
        self::QUERY_TYPE_UID            => ['name' => 'UID',    'valStr' => "\x00\x65"],
        self::QUERY_TYPE_GID            => ['name' => 'GID',    'valStr' => "\x00\x66"],
        self::QUERY_TYPE_ANY            => ['name' => 'ANY',    'valStr' => "\x00\xFF"],
    ];
}